
package com.trydofor.id.a9id;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.ReentrantLock;

import javax.annotation.concurrent.ThreadSafe;

import com.trydofor.id.core.A9CQ;
import com.trydofor.id.core.ApiExceptions;

/**
 * a serial based a9id maker.
 * 
 * <pre>
 * in the SERIAL-BASED maker, the SECOND increases within the SERIAL.
 * - in the same second, the serial increases by increment.
 * - in the next second, the serial does nothing.
 * - reaching the end, the serial resets.
 * 
 * in the SECOND-BASED maker, the SERIAL increases within the SECOND.
 * - in the same second,the serial increases by increment.
 * - in the next second,the serial resets.
 * </pre>
 * 
 * @author Shi Rongjiu (www.trydofor.com)
 * @since 0.9.4
 */
@ThreadSafe
public class A9idMixedMaker extends A9idAbstractMaker {
    
    private final ReentrantLock lock = new ReentrantLock();
    
    private final boolean       secondBased;
    // // sync ////
    private final A9CQ<Integer> currentA9cq;
    private volatile long       currentScnd;               // Out-of-thin-air 
                                                            
    /**
     * new a serial-based thread-safe instance.<br>
     * the serial increases within second, and do nothing in the next second.
     * 
     * <b>NOTE</b>: may deadlock if sync a9cq outside.
     * 
     * @param worker the worker
     * @param epoch SECOND start with (System.currentTimeMillis()/1000-epoch)
     * @param a9cq the outside a9cq.
     * @param secondBased second or serial based.
     * @throws IllegalArgumentException if parameters out of range.
     * @throws NullPointerException if a9cq is null
     */
    public A9idMixedMaker(int worker, long epoch, A9CQ<Integer> a9cq, boolean secondBased) {
        super(worker, epoch);
        ApiExceptions.checkNullPointer(a9cq);
        
        this.currentScnd = currentSecond();
        this.currentA9cq = a9cq;
        this.secondBased = secondBased;
    }
    
    /**
     * protected by ReentrantLock.
     * 
     * {@inheritDoc}
     */
    public A9ID make() {
        A9ID r = null;
        try {
            r = make(0, null);
        }
        catch (InterruptedException e) {
            ApiExceptions.throwFakeException(e);
        }
        catch (TimeoutException e) {
            ApiExceptions.throwFakeException(e);
        }
        return r;
    }
    
    /**
     * protected by ReentrantLock.
     * 
     * {@inheritDoc}
     */
    public A9ID make(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException {
        
        final long startedNano = System.nanoTime();
        final Thread curThread = Thread.currentThread();
        final boolean needTime = (timeout > 0 && unit != null);
        
        final long timesecond;
        final Integer tssequence;
        
        lock.lock();
        try {
            // pre-check
            if (needTime) {
                ApiExceptions.checkThreadInterrupted(curThread);
                ApiExceptions.checkTimeoutNano(startedNano, timeout, unit);
            }
            final long nowScnd = currentSecond();
            timesecond = nowScnd - epoch + 1;
            
            if (secondBased) {
                if (nowScnd > currentScnd) { // reset
                    currentA9cq.reset(); // *!atomic!*
                    currentScnd = nowScnd;
                }
                tssequence = currentA9cq.next(); // *!atomic!*
            }
            else {
                if (nowScnd == currentScnd) { // next
                    Integer cq = currentA9cq.next(); // *!atomic!*
                    if (cq == null) {
                        currentA9cq.reset();
                        tssequence = currentA9cq.next(); // *!atomic!*
                    }
                    else {
                        tssequence = cq;
                    }
                }
                else {
                    tssequence = currentA9cq.current(); // *!atomic!*
                    currentScnd = nowScnd;
                }
            }
        }
        finally {
            lock.unlock();
        }
        
        // post-check
        if (needTime) {
            ApiExceptions.checkThreadInterrupted(curThread);
            ApiExceptions.checkTimeoutNano(startedNano, timeout, unit);
        }
        
        return tssequence == null ? null : new A9ID(worker, timesecond, tssequence); // cost 156(ns)
    }
    
    @Override
    public String toString() {
        return getClass() + ", secondBased=" + secondBased + ", currentA9cq=" + currentA9cq
               + ", currentScnd=" + currentScnd + ", worker=" + worker + ", epoch=" + epoch;
    }
}
